/**
 * Provides classes and predicates for reasoning about data flow through the redux package.
 */

import javascript
private import semmle.javascript.dataflow.internal.PreCallGraphStep
private import semmle.javascript.Unit

/**
 * Provides classes and predicates for reasoning about data flow through the redux package.
 */
module Redux {
  /**
   * To avoid mixing up the state between independent Redux apps that live in a monorepo,
   * we do a heuristic program slicing based on `package.json` files. For most projects this has no effect.
   */
  private module ProgramSlicing {
    /** Gets the innermost `package.json` file in a directory containing the given file. */
    private PackageJson getPackageJson(Container f) {
      f = result.getFile().getParentContainer()
      or
      not exists(f.getFile("package.json")) and
      result = getPackageJson(f.getParentContainer())
    }

    private predicate packageDependsOn(PackageJson importer, PackageJson dependency) {
      importer.getADependenciesObject("").getADependency(dependency.getPackageName(), _)
    }

    /** Gets a package that can be considered an entry point for a Redux app. */
    private PackageJson entryPointPackage() {
      result = getPackageJson(any(StoreCreation c).getFile())
      or
      // Any package that imports a store-creating package is considered a potential entry point.
      packageDependsOn(result, entryPointPackage())
    }

    pragma[nomagic]
    private predicate arePackagesInSameReduxApp(PackageJson a, PackageJson b) {
      exists(PackageJson entry |
        entry = entryPointPackage() and
        packageDependsOn*(entry, a) and
        packageDependsOn*(entry, b)
      )
    }

    /** Holds if the two files are considered to be part of the same Redux app. */
    pragma[inline]
    predicate areFilesInSameReduxApp(File a, File b) {
      not exists(PackageJson pkg)
      or
      arePackagesInSameReduxApp(getPackageJson(a), getPackageJson(b))
    }
  }

  /**
   * A creation of a redux store, usually via a call to `createStore`.
   */
  class StoreCreation extends DataFlow::SourceNode instanceof StoreCreation::Range {
    /** Gets a reference to the store. */
    DataFlow::SourceNode ref() { result = this.asApiNode().getAValueReachableFromSource() }

    /** Gets an API node that refers to this store creation. */
    API::Node asApiNode() { result.asSource() = this }

    /** Gets the data flow node holding the root reducer for this store. */
    DataFlow::Node getReducerArg() { result = super.getReducerArg() }

    /** Gets a data flow node referring to the root reducer. */
    DataFlow::SourceNode getAReducerSource() {
      result = this.getReducerArg().(ReducerArg).getASource()
    }
  }

  /** Companion module to the `StoreCreation` class. */
  module StoreCreation {
    /**
     * The creation of a redux store. Additional `StoreCreation` instances can be generated by subclassing this class.
     */
    abstract class Range extends DataFlow::SourceNode {
      /** Gets the data flow node holding the root reducer for this store. */
      abstract DataFlow::Node getReducerArg();
    }

    private class CreateStore extends DataFlow::CallNode, Range {
      CreateStore() {
        this = API::moduleImport(["redux", "@reduxjs/toolkit"]).getMember("createStore").getACall()
      }

      override DataFlow::Node getReducerArg() { result = this.getArgument(0) }
    }

    private class ToolkitStore extends API::CallNode, Range {
      ToolkitStore() {
        this = API::moduleImport("@reduxjs/toolkit").getMember("configureStore").getACall()
      }

      override DataFlow::Node getReducerArg() {
        result = this.getParameter(0).getMember("reducer").asSink()
      }
    }
  }

  /** An API node that is a source of the Redux root state. */
  abstract private class RootStateSource extends API::Node { }

  /** Gets an API node referring to the Redux root state. */
  private API::Node rootState() {
    result instanceof RootStateSource
    or
    stateStep(rootState().getAValueReachableFromSource(), result.asSource())
  }

  /**
   * Gets an API node referring to the given (non-empty) access path within the Redux state.
   */
  private API::Node rootStateAccessPath(string accessPath) {
    result = rootState().getMember(accessPath)
    or
    exists(string base, string prop |
      result = rootStateAccessPath(base).getMember(prop) and
      accessPath = joinAccessPaths(base, prop)
    )
    or
    stateStep(rootStateAccessPath(accessPath).getAValueReachableFromSource(), result.asSource())
  }

  /**
   * Combines two state access paths, while disallowing unbounded growth of access paths.
   */
  bindingset[base, prop]
  private string joinAccessPaths(string base, string prop) {
    result = base + "." + prop and
    // Allow at most two occurrences of a given property name in the path
    // (one in the base, plus the one we're appending now).
    count(base.indexOf("." + prop + ".")) <= 1
  }

  /**
   * The creation of a reducer function that delegates to one or more other reducer functions.
   *
   * Delegating reducers can delegate specific parts of the state object (`getStateHandlerArg`),
   * actions of a specific type (`getActionHandlerArg`), or everything (`getAPlainHandlerArg`).
   */
  abstract class DelegatingReducer extends DataFlow::SourceNode {
    /**
     * Gets a data flow node holding a reducer to which handling of `state.prop` is delegated.
     *
     * For example, gets the `fn` in `combineReducers({foo: fn})` with `prop` bound to `foo`.
     *
     * The delegating reducer should behave as a function of this form:
     * ```js
     * function outer(state, action) {
     *   return {
     *     prop: inner(state.prop, action),
     *     ...
     *   }
     * }
     * ```
     */
    DataFlow::Node getStateHandlerArg(string prop) { none() }

    /**
     * Gets a data flow node holding a reducer to which actions of the given type are delegated.
     *
     * For example, gets the `fn` in `handleAction(a, fn)` with `actionType` bound to `a`.
     *
     * The `actionType` node may refer an action creator or a string value corresponding to `action.type`.
     */
    DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) { none() }

    /**
     * Gets a data flow node holding a reducer to which every request is forwarded (for the
     * purpose of this model).
     *
     * For example, gets the `fn` in `persistReducer(config, fn)`.
     */
    DataFlow::Node getAPlainHandlerArg() { none() }

    /** Gets the use site of this reducer. */
    final ReducerArg getUseSite() { result.getASource() = this }
  }

  private module DelegatingReducer {
    private API::Node combineReducers() {
      result =
        API::moduleImport(["redux", "redux-immutable", "@reduxjs/toolkit"])
            .getMember("combineReducers")
    }

    /**
     * A call to `combineReducers`, which delegates properties of `state` to individual sub-reducers.
     */
    private class CombineReducers extends API::CallNode, DelegatingReducer {
      CombineReducers() { this = combineReducers().getACall() }

      override DataFlow::Node getStateHandlerArg(string prop) {
        result = this.getParameter(0).getMember(prop).asSink()
      }
    }

    /**
     * An object literal flowing into a nested property in a `combineReducers` object, such as the `{ bar }` object in:
     * ```js
     * combineReducers({ foo: { bar } })
     * ```
     *
     * Although the object itself is clearly not a function, we use the object to model the corresponding reducer function created by `combineReducers`.
     */
    private class NestedCombineReducers extends DelegatingReducer, DataFlow::ObjectLiteralNode {
      NestedCombineReducers() {
        this = combineReducers().getParameter(0).getAMember+().getAValueReachingSink()
      }

      override DataFlow::Node getStateHandlerArg(string prop) {
        result = this.getAPropertyWrite(prop).getRhs()
      }
    }

    /**
     * A call to `handleActions`, creating a reducer function that dispatched based on the action type:
     *
     * ```js
     * let reducer = handleActions({
     *   actionType1: (state, action) => { ... },
     *   actionType2: (state, action) => { ... },
     * })
     * ```
     */
    private class HandleActions extends API::CallNode, DelegatingReducer {
      HandleActions() {
        this =
          API::moduleImport(["redux-actions", "redux-ts-utils"])
              .getMember("handleActions")
              .getACall()
      }

      override DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) {
        exists(DataFlow::PropWrite write |
          result = this.getParameter(0).getAMember().asSink() and
          write.getRhs() = result and
          actionType = write.getPropertyNameExpr().flow()
        )
      }
    }

    /**
     * A call to `handleAction`, creating a reducer function that only handles a given action type:
     *
     * ```js
     * let reducer = handleAction('actionType', (state, action) => { ... });
     * ```
     */
    private class HandleAction extends API::CallNode, DelegatingReducer {
      HandleAction() {
        this =
          API::moduleImport(["redux-actions", "redux-ts-utils"])
              .getMember("handleAction")
              .getACall()
      }

      override DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) {
        actionType = this.getArgument(0) and
        result = this.getArgument(1)
      }
    }

    /**
     * A call to `persistReducer`, which we model as a plain wrapper around another reducer.
     */
    private class PersistReducer extends DataFlow::CallNode, DelegatingReducer {
      PersistReducer() {
        this = API::moduleImport("redux-persist").getMember("persistReducer").getACall()
      }

      override DataFlow::Node getAPlainHandlerArg() { result = this.getArgument(1) }
    }

    /**
     * A call to `immer` or `immer.produce`, which we model as a plain wrapper around another reducer.
     */
    private class ImmerProduce extends DataFlow::CallNode, DelegatingReducer {
      ImmerProduce() {
        this = API::moduleImport("immer").getACall()
        or
        this = API::moduleImport("immer").getMember("produce").getACall()
      }

      override DataFlow::Node getAPlainHandlerArg() { result = this.getArgument(0) }
    }

    /**
     * A call to `reduce-reducers`, modeled as a reducer that dispatches to an arbitrary subreducer.
     *
     * In reality, this function chains together all of the reducers, but in practice it is only used
     * when the reducers handle a disjoint set of action types, which makes it behave as if it
     * dispatched to just one of them.
     *
     * For example:
     * ```js
     * let reducer = reduceReducers([
     *   handleAction('action1', (state, action) => { ... }),
     *   handleAction('action2', (state, action) => { ... }),
     * ]);
     * ```
     */
    private class ReduceReducers extends DataFlow::CallNode, DelegatingReducer {
      ReduceReducers() {
        this = API::moduleImport("reduce-reducers").getACall() or
        this =
          API::moduleImport(["redux-actions", "redux-ts-utils"])
              .getMember("reduceReducers")
              .getACall()
      }

      override DataFlow::Node getAPlainHandlerArg() {
        result = this.getAnArgument()
        or
        result = this.getArgument(0).getALocalSource().(DataFlow::ArrayCreationNode).getAnElement()
      }
    }

    /**
     * A call to `createReducer`, for example:
     *
     * ```js
     * let reducer = createReducer(initialState, (builder) => {
     *   builder
     *     .addCase(actionType1, (state, action) => { ... })
     *     .addCase(actionType2, (state, action) => { ... });
     * });
     * ```
     */
    private class CreateReducer extends API::CallNode, DelegatingReducer {
      CreateReducer() {
        this = API::moduleImport("@reduxjs/toolkit").getMember("createReducer").getACall()
      }

      private API::Node getABuilderRef() {
        result = this.getParameter(1).getParameter(0)
        or
        result = this.getABuilderRef().getAMember().getReturn()
      }

      override DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) {
        exists(API::CallNode addCase |
          addCase = this.getABuilderRef().getMember("addCase").getACall() and
          actionType = addCase.getArgument(0) and
          result = addCase.getArgument(1)
        )
      }
    }

    /**
     * A reducer created by a call to `createSlice`. Note that `createSlice` creates both
     * reducers and actions; this class models the reducers only.
     *
     * For example:
     * ```js
     * let slice = createSlice({
     *   name: 'mySlice',
     *   reducers: {
     *     actionType1: (state, action) => { ... },
     *     actionType2: (state, action) => { ... },
     *   },
     *   extraReducers: (builder) => {
     *     builder.addCase('actionType3', (state, action) => { ... })
     *   }
     * });
     * export default slice.reducer;
     * export const { actionType1, actionType2 } = slice.actions;
     * ```
     */
    private class CreateSliceReducer extends DelegatingReducer {
      API::CallNode call;

      CreateSliceReducer() {
        call = API::moduleImport("@reduxjs/toolkit").getMember("createSlice").getACall() and
        this = call.getReturn().getMember("reducer").asSource()
      }

      private API::Node getABuilderRef() {
        result = call.getParameter(0).getMember("extraReducers").getParameter(0)
        or
        result = this.getABuilderRef().getAMember().getReturn()
      }

      override DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) {
        exists(string name |
          result = call.getParameter(0).getMember("reducers").getMember(name).asSink() and
          actionType = call.getReturn().getMember("actions").getMember(name).asSource()
        )
        or
        // Properties of 'extraReducers':
        //   { extraReducers: { [action]: reducer }}
        exists(DataFlow::PropWrite write |
          result = call.getParameter(0).getMember("extraReducers").getAMember().asSink() and
          write.getRhs() = result and
          actionType = write.getPropertyNameExpr().flow()
        )
        or
        // Builder callback to 'extraReducers':
        //   extraReducers: builder => builder.addCase(action, reducer)
        exists(API::CallNode addCase |
          addCase = this.getABuilderRef().getMember("addCase").getACall() and
          actionType = addCase.getArgument(0) and
          result = addCase.getArgument(1)
        )
      }
    }
  }

  /**
   * A function for creating and dispatching action objects of shape `{type, payload}`.
   *
   * In the simplest case, an action creator is a function, which, for some string `T` behaves as the function `x => {type: T, payload: x}`.
   *
   * An action creator may have a middleware function `f`, which makes it behave as the function `x => {type: T, payload: f(x)}` (that is,
   * the function `f` converts the argument into the actual payload).
   *
   * Some action creators dispatch the action to a store, while for others, the value is returned and it is simply assumed to be dispatched
   * at some point. We model all action creators as if they dispatch the action they create.
   */
  class ActionCreator extends DataFlow::SourceNode instanceof ActionCreator::Range {
    /** Gets the `type` property of actions created by this action creator, if it is known. */
    string getTypeTag() { result = super.getTypeTag() }

    /**
     * Gets the middleware function that transforms arguments passed to this function into the
     * action payload.
     *
     * Not every action creator has a middleware function; in such cases the first argument is
     * treated as the action payload.
     *
     * If `async` is true, the middlware function returns a promise whose value eventually becomes
     * the action payload. Otherwise, the return value is the payload itself.
     */
    DataFlow::FunctionNode getMiddlewareFunction(boolean async) {
      result = super.getMiddlewareFunction(async)
    }

    /** Gets a data flow node referring to this action creator. */
    private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
      t.start() and
      result = this
      or
      // x -> bindActionCreators({ x, ... })
      exists(BindActionCreatorsCall bind, string prop |
        this.ref(t.continue()).flowsTo(bind.getParameter(0).getMember(prop).asSink()) and
        result = bind.getReturn().getMember(prop).asSource()
      )
      or
      // x -> combineActions(x, ...)
      exists(API::CallNode combiner |
        combiner =
          API::moduleImport(["redux-actions", "redux-ts-utils"])
              .getMember("combineActions")
              .getACall() and
        this.ref(t.continue()).flowsTo(combiner.getAnArgument()) and
        result = combiner
      )
      or
      // x -> x.fulfilled, for async action creators
      result = this.ref(t.continue()).getAPropertyRead("fulfilled")
      or
      // follow flow through mapDispatchToProps
      ReactRedux::dispatchToPropsStep(this.ref(t.continue()).getALocalUse(), result)
      or
      exists(DataFlow::TypeTracker t2 | result = this.ref(t2).track(t2, t))
    }

    /** Gets a data flow node referring to this action creator. */
    DataFlow::SourceNode ref() { result = this.ref(DataFlow::TypeTracker::end()) }

    /**
     * Gets a block that is executed when a check has determined that `action` originated from this action creator.
     */
    private ReachableBasicBlock getASuccessfulTypeCheckBlock(DataFlow::SourceNode action) {
      action = getAnUntypedActionInReducer() and
      result = getASuccessfulTypeCheckBlock(action, this.getTypeTag())
      or
      // some action creators implement a .match method for this purpose
      exists(ConditionGuardNode guard, DataFlow::CallNode call |
        call = this.ref().getAMethodCall("match") and
        guard.getTest() = call.asExpr() and
        action.flowsTo(call.getArgument(0)) and
        guard.getOutcome() = true and
        result = guard.getBasicBlock()
      )
    }

    /**
     * Gets a reducer that handles the type of action created by this action creator, for example:
     * ```js
     * handleAction(TYPE, (state, action) => { ... action.payload ... })
     * ```
     *
     * Does not include reducers that perform their own action type checking.
     */
    DataFlow::FunctionNode getAReducerFunction() {
      exists(ReducerArg reducer |
        reducer.isTypeTagHandler(this.getTypeTag())
        or
        reducer.isActionTypeHandler(this.ref().getALocalUse())
      |
        result = reducer.getASource()
      )
    }

    /** Gets a data flow node referring a payload of this action (usually in the reducer function). */
    DataFlow::SourceNode getAPayloadReference() {
      // `if (action.type === TYPE) { ... action.payload ... }`
      exists(DataFlow::SourceNode actionSrc |
        actionSrc = getAnUntypedActionInReducer() and
        result = actionSrc.getAPropertyRead("payload") and
        this.getASuccessfulTypeCheckBlock(actionSrc).dominates(result.getBasicBlock())
      )
      or
      result = this.getAReducerFunction().getParameter(1).getAPropertyRead("payload")
    }

    /** Gets a data flow node referring to the first argument of the action creator invocation. */
    DataFlow::SourceNode getAMetaArgReference() {
      exists(ReducerArg reducer |
        reducer
            .isActionTypeHandler(this.ref().getAPropertyRead(["fulfilled", "rejected", "pending"])) and
        result =
          reducer
              .getASource()
              .(DataFlow::FunctionNode)
              .getParameter(1)
              .getAPropertyRead("meta")
              .getAPropertyRead("arg")
      )
    }
  }

  /** Companion module to the `ActionCreator` class. */
  module ActionCreator {
    /** A function for creating and dispatching action objects of shape `{type, payload}`. */
    abstract class Range extends DataFlow::SourceNode {
      /** Gets the `type` property of actions created by this action creator */
      abstract string getTypeTag();

      /** Gets the function transforming arguments into the action payload. */
      DataFlow::FunctionNode getMiddlewareFunction(boolean async) { none() }
    }

    /**
     * An action creator made using `createAction`:
     * ```js
     * let action1 = createAction('action1');
     * let action2 = createAction('action2', (x,y) => { x, y });
     * ```
     */
    private class SingleAction extends Range, API::CallNode {
      SingleAction() {
        this =
          API::moduleImport(["@reduxjs/toolkit", "redux-actions", "redux-ts-utils"])
              .getMember("createAction")
              .getACall()
      }

      override string getTypeTag() { this.getArgument(0).mayHaveStringValue(result) }

      override DataFlow::FunctionNode getMiddlewareFunction(boolean async) {
        result = this.getCallback(1) and async = false
      }
    }

    /**
     * An action creator made by a call to `createActions`:
     * ```js
     * let { actionOne, actionTwo } = createActions({
     *   ACTION_ONE: (x, y) => { x, y },
     *   ACTION_TWO: (x, y) => { x, y },
     * })
     * ```
     */
    class MultiAction extends Range {
      API::CallNode createActions;
      string name;

      MultiAction() {
        createActions = API::moduleImport("redux-actions").getMember("createActions").getACall() and
        this = createActions.getReturn().getMember(name).asSource()
      }

      override DataFlow::FunctionNode getMiddlewareFunction(boolean async) {
        result.flowsTo(createActions.getParameter(0).getMember(this.getTypeTag()).asSink()) and
        async = false
      }

      override string getTypeTag() {
        result = name.regexpReplaceAll("([a-z])([A-Z])", "$1_$2").toUpperCase()
      }
    }

    /**
     * An action creator made by a call to `createSlice`. Note that `createSlice` creates both
     * reducers and actions; this class models the action creators.
     *
     * ```js
     * let slice = createSlice({
     *   name: 'mySlice',
     *   reducers: {
     *     actionType1: (state, action) => { ... },
     *     actionType2: (state, action) => { ... },
     *   },
     * });
     * export const { actionType1, actionType2 } = slice.actions;
     * ```
     */
    private class CreateSliceAction extends Range {
      API::CallNode call;
      string actionName;

      CreateSliceAction() {
        call = API::moduleImport("@reduxjs/toolkit").getMember("createSlice").getACall() and
        this = call.getReturn().getMember("actions").getMember(actionName).asSource()
      }

      override string getTypeTag() {
        exists(string prefix |
          call.getParameter(0).getMember("name").asSink().mayHaveStringValue(prefix) and
          result = prefix + "/" + actionName
        )
      }
    }

    /**
     * An action creator made by a call to `createAsyncThunk`:
     * ```js
     * const fetchUserId = createAsyncThunk('fetchUserId', async (id) => {
     *   return (await fetchUserId(id)).data;
     * });
     * ```
     */
    private class CreateAsyncThunk extends Range, API::CallNode {
      CreateAsyncThunk() {
        this = API::moduleImport("@reduxjs/toolkit").getMember("createAsyncThunk").getACall()
      }

      override DataFlow::FunctionNode getMiddlewareFunction(boolean async) {
        async = true and
        result = this.getParameter(1).getAValueReachingSink()
      }

      override string getTypeTag() { this.getArgument(0).mayHaveStringValue(result) }
    }
  }

  /**
   * Gets the type tag of an action creator reaching `node`.
   */
  private string getAnActionTypeTag(DataFlow::SourceNode node) {
    exists(ActionCreator action |
      node = action.ref() and
      result = action.getTypeTag()
    )
  }

  /** Gets the type tag of an action reaching `node`, or the string value of `node`. */
  // Inlined to avoid duplicating `mayHaveStringValue`
  pragma[inline]
  private string getATypeTagFromNode(DataFlow::Node node) {
    node.mayHaveStringValue(result)
    or
    node.asExpr().(Label).getName() = result
    or
    result = getAnActionTypeTag(node.getALocalSource())
  }

  /** A data flow node that is used as a reducer. */
  class ReducerArg extends DataFlow::Node {
    ReducerArg() {
      this = any(StoreCreation c).getReducerArg()
      or
      this = any(DelegatingReducer r).getStateHandlerArg(_)
      or
      this = any(DelegatingReducer r).getActionHandlerArg(_)
    }

    /** Gets a data flow node that flows to this reducer argument. */
    DataFlow::SourceNode getASource(DataFlow::TypeBackTracker t) {
      t.start() and
      result = this.getALocalSource()
      or
      // Step through forwarding functions
      DataFlow::functionForwardingStep(result.getALocalUse(), this.getASource(t.continue()))
      or
      // Step through library functions like `redux-persist`
      result.getALocalUse() =
        this.getASource(t.continue()).(DelegatingReducer).getAPlainHandlerArg()
      or
      // Step through function composition (usually composed with various state "enhancer" functions)
      exists(FunctionCompositionCall compose, DataFlow::CallNode call |
        this.getASource(t.continue()) = call and
        call = compose.getACall() and
        result.getALocalUse() = [compose.getAnOperandNode(), call.getAnArgument()]
      )
      or
      exists(DataFlow::TypeBackTracker t2 | result = this.getASource(t2).backtrack(t2, t))
    }

    /** Gets a data flow node that flows to this reducer argument. */
    DataFlow::SourceNode getASource() { result = this.getASource(DataFlow::TypeBackTracker::end()) }

    /**
     * Holds if the actions dispatched to this reducer have the given type, that is,
     * it is created by an action creator that flows to `actionType`, or has `action.type` set to
     * the string value of `actionType`.
     */
    predicate isActionTypeHandler(DataFlow::Node actionType) {
      exists(DelegatingReducer r |
        this = r.getActionHandlerArg(actionType)
        or
        this = r.getStateHandlerArg(_) and
        r.getUseSite().isActionTypeHandler(actionType)
      )
    }

    /**
     * Holds if the actions dispatched to this reducer have the given `action.type` value.
     */
    predicate isTypeTagHandler(string actionType) {
      exists(DataFlow::Node node |
        this.isActionTypeHandler(node) and
        actionType = getATypeTagFromNode(node)
      )
    }

    /**
     * Holds if this reducer operates on the root state, as opposed to some access path within the state.
     */
    predicate isRootStateHandler() {
      this = any(StoreCreation c).getReducerArg()
      or
      exists(DelegatingReducer r |
        this = r.getActionHandlerArg(_) and
        r.getUseSite().isRootStateHandler()
      )
    }
  }

  /**
   * A source of the `dispatch` function, used as starting point for `getADispatchFunctionNode`.
   */
  abstract private class DispatchFunctionSource extends API::Node { }

  /**
   * A value that is dispatched, that is, flows to the first argument of `dispatch`
   * (but where the call to `dispatch` is not necessarily explicit in the code).
   *
   * Used as starting point for `getADispatchedValueNode`.
   */
  abstract private class DispatchedValueSink extends API::Node { }

  /** Gets an API node referring to the Redux `dispatch` function. */
  API::Node getADispatchFunctionNode() {
    result instanceof DispatchFunctionSource
    or
    result = getADispatchedValueNode().getParameter(0)
  }

  /** Gets an API node corresponding to a value being passed to the `dispatch` function. */
  API::Node getADispatchedValueNode() {
    result instanceof DispatchedValueSink
    or
    result = getADispatchFunctionNode().getParameter(0)
  }

  private class StoreDispatchSource extends DispatchFunctionSource {
    StoreDispatchSource() { this = any(StoreCreation c).asApiNode().getMember("dispatch") }
  }

  /** Gets the `action` parameter of a reducer that isn't behind an implied type guard. */
  DataFlow::SourceNode getAnUntypedActionInReducer() {
    exists(ReducerArg reducer |
      not reducer.isTypeTagHandler(_) and
      result = reducer.getASource().(DataFlow::FunctionNode).getParameter(1)
    )
  }

  /** A call to `bindActionCreators` */
  private class BindActionCreatorsCall extends API::CallNode {
    BindActionCreatorsCall() {
      this =
        API::moduleImport(["redux", "@reduxjs/toolkit"]).getMember("bindActionCreators").getACall()
    }
  }

  /** The return value of a function flowing into `bindActionCreators`, seen as a value that is dispatched. */
  private class BindActionDispatchSink extends DispatchedValueSink {
    BindActionDispatchSink() {
      this = any(BindActionCreatorsCall c).getParameter(0).getAMember().getReturn()
    }
  }

  /**
   * Holds if `pred -> succ` is step from an action creation to its use in a reducer function.
   */
  predicate actionToReducerStep(DataFlow::Node pred, DataFlow::Node succ) {
    // Actions created by an action creator library
    exists(ActionCreator action |
      exists(DataFlow::CallNode call | call = action.ref().getACall() |
        exists(int i |
          pred = call.getArgument(i) and
          succ = action.getMiddlewareFunction(_).getParameter(i)
        )
        or
        not exists(action.getMiddlewareFunction(_)) and
        pred = call.getArgument(0) and
        succ = action.getAPayloadReference()
        or
        pred = call.getArgument(0) and
        succ = action.getAMetaArgReference()
      )
      or
      pred = action.getMiddlewareFunction(false).getReturnNode() and
      succ = action.getAPayloadReference()
    )
    or
    // Manually created and dispatched actions
    exists(string actionType, string prop, DataFlow::SourceNode actionSrc |
      actionSrc = getAnUntypedActionInReducer() and
      pred = getAManuallyDispatchedValue(actionType).getAPropertyWrite(prop).getRhs() and
      succ = actionSrc.getAPropertyRead(prop)
    |
      getASuccessfulTypeCheckBlock(actionSrc, actionType).dominates(succ.getBasicBlock())
      or
      exists(ReducerArg reducer |
        reducer.isTypeTagHandler(actionType) and
        actionSrc = reducer.getASource().(DataFlow::FunctionNode).getParameter(1)
      )
    )
  }

  /** Holds if `pred -> succ` is a step from the promise of an action payload to its use in a reducer function. */
  predicate actionToReducerPromiseStep(DataFlow::Node pred, DataFlow::SourceNode succ) {
    exists(ActionCreator action |
      pred = action.getMiddlewareFunction(true).getReturnNode() and
      succ = action.getAPayloadReference()
    )
  }

  private class ActionToReducerStep extends DataFlow::SharedFlowStep {
    override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
      actionToReducerStep(pred, succ)
    }

    override predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
      actionToReducerPromiseStep(pred, succ) and prop = Promises::valueProp()
    }
  }

  /** Gets the access path which `reducer` operates on. */
  string getAffectedStateAccessPath(ReducerArg reducer) {
    exists(DelegatingReducer r |
      exists(string prop | reducer = r.getStateHandlerArg(prop) |
        result = joinAccessPaths(getAffectedStateAccessPath(r.getUseSite()), prop)
        or
        r.getUseSite().isRootStateHandler() and
        result = prop
      )
      or
      reducer = r.getActionHandlerArg(_) and
      result = getAffectedStateAccessPath(r.getUseSite())
    )
  }

  /**
   * Holds if `pred -> succ` should be a step from a reducer to a state access affected by the reducer.
   */
  predicate reducerToStateStep(DataFlow::Node pred, DataFlow::Node succ) {
    reducerToStateStepAux(pred, succ) and
    ProgramSlicing::areFilesInSameReduxApp(pred.getFile(), succ.getFile())
  }

  /**
   * Holds if `pred -> succ` should be a step from a reducer to a state access affected by the reducer.
   *
   * This is a helper predicate for `reducerToStateStep` without the program-slicing check.
   */
  pragma[nomagic]
  private predicate reducerToStateStepAux(DataFlow::Node pred, DataFlow::SourceNode succ) {
    exists(ReducerArg reducer, DataFlow::FunctionNode function, string accessPath |
      function = reducer.getASource() and
      accessPath = getAffectedStateAccessPath(reducer)
    |
      pred = function.getReturnNode() and
      succ = rootStateAccessPath(accessPath).asSource()
      or
      exists(string suffix, DataFlow::SourceNode base |
        base = [function.getParameter(0), function.getReturnNode().getALocalSource()] and
        pred = AccessPath::getAnAssignmentTo(base, suffix) and
        succ = rootStateAccessPath(accessPath + "." + suffix).asSource()
      )
    )
    or
    exists(
      ReducerArg reducer, DataFlow::FunctionNode function, string suffix, DataFlow::SourceNode base
    |
      function = reducer.getASource() and
      reducer.isRootStateHandler() and
      base = [function.getParameter(0), function.getReturnNode().getALocalSource()] and
      pred = AccessPath::getAnAssignmentTo(base, suffix) and
      succ = rootStateAccessPath(suffix).asSource()
    )
  }

  private class ReducerToStateStep extends DataFlow::SharedFlowStep {
    override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
      reducerToStateStep(pred, succ)
    }
  }

  /**
   * Gets a dispatched object literal with a property `type: actionType`.
   */
  private DataFlow::ObjectLiteralNode getAManuallyDispatchedValue(string actionType) {
    result.getAPropertyWrite("type").getRhs().mayHaveStringValue(actionType) and
    result = getADispatchedValueNode().getAValueReachingSink()
  }

  /**
   * Gets the block to be executed after a check has determined that `action.type` is `actionType`,
   * or the entry block of a closure dominated by such a check.
   */
  private ReachableBasicBlock getASuccessfulTypeCheckBlock(
    DataFlow::SourceNode action, string actionType
  ) {
    action = getAnUntypedActionInReducer() and
    (
      exists(MembershipCandidate candidate, ConditionGuardNode guard |
        action.getAPropertyRead("type").flowsTo(candidate) and
        candidate.getAMemberString() = actionType and
        guard.getTest() = candidate.getTest().asExpr() and
        guard.getOutcome() = candidate.getTestPolarity() and
        result = guard.getBasicBlock()
      )
      or
      exists(SwitchStmt switch, SwitchCase case |
        action.getAPropertyRead("type").flowsTo(switch.getExpr().flow()) and
        case = switch.getACase() and
        case.getExpr().mayHaveStringValue(actionType) and
        result = getCaseBlock(case)
      )
    )
    or
    exists(Function f |
      getASuccessfulTypeCheckBlock(action, actionType)
          .dominates(f.(ControlFlowNode).getBasicBlock()) and
      result = f.getEntryBB()
    )
  }

  /** Gets the block to execute when `case` matches successfully. */
  private BasicBlock getCaseBlock(SwitchCase case) {
    result = case.getBodyStmt(0).getBasicBlock()
    or
    not exists(case.getABodyStmt()) and
    exists(SwitchStmt stmt, int i |
      stmt.getCase(i) = case and
      result = getCaseBlock(stmt.getCase(i + 1))
    )
  }

  /**
   * Defines a flow step to be used for propagating tracking access to `state`.
   *
   * A `SharedFlowStep` is generated for these steps as well.
   * It is distinct from `SharedFlowStep` to avoid recursion between that and the propagation of `state`.
   */
  private class StateStep extends Unit {
    abstract predicate step(DataFlow::Node pred, DataFlow::Node succ);
  }

  private predicate stateStep(DataFlow::Node pred, DataFlow::Node succ) {
    any(StateStep s).step(pred, succ)
  }

  private class StateStepAsFlowStep extends DataFlow::SharedFlowStep {
    override predicate step(DataFlow::Node pred, DataFlow::Node succ) { stateStep(pred, succ) }
  }

  /**
   * Model of the `react-redux` package.
   */
  private module ReactRedux {
    /** Gets an API node referring to the `useSelector` function. */
    API::Node useSelector() {
      result = API::moduleImport("react-redux").getMember("useSelector").getForwardingFunction*()
    }

    /**
     * A step out of a `useSelector` call, such as from `state.x` to the result of `useSelector(state => state.x)`.
     */
    class UseSelectorStep extends StateStep {
      override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
        exists(API::CallNode call |
          call = useSelector().getACall() and
          pred = call.getCallback(0).getReturnNode() and
          succ = call
        )
      }
    }

    /** The argument to a `useSelector` callback, seen as a root state reference. */
    class UseSelectorStateSource extends RootStateSource {
      UseSelectorStateSource() { this = useSelector().getParameter(0).getParameter(0) }
    }

    /** A call to `useDispatch`, as a source of the `dispatch` function. */
    private class UseDispatchFunctionSource extends DispatchFunctionSource {
      UseDispatchFunctionSource() {
        this = API::moduleImport("react-redux").getMember("useDispatch").getReturn()
      }
    }

    /**
     * A call to `connect()`, typically as part of a code pattern like the following:
     * ```js
     * let withConnect = connect(mapStateToProps, mapDispatchToProps);
     * let MyAwesomeComponent = compose(withConnect, otherStuff)(MyComponent);
     * ```
     */
    abstract private class ConnectCall extends API::CallNode {
      /** Gets the API node corresponding to the `mapStateToProps` argument. */
      abstract API::Node getMapStateToProps();

      /** Gets the API node corresponding to the `mapDispatchToProps` argument. */
      abstract API::Node getMapDispatchToProps();

      /**
       * Gets a function whose first argument becomes the React component to connect.
       */
      DataFlow::SourceNode getAComponentTransformer() {
        result = this
        or
        exists(FunctionCompositionCall compose |
          this.getAComponentTransformer().flowsTo(compose.getAnOperandNode()) and
          result = compose
        )
      }

      /**
       * Gets a data-flow node that should flow to `props.name` via the `mapDispatchToProps` function.
       */
      DataFlow::Node getDispatchPropNode(string name) {
        // Implicitly bound by bindActionCreators:
        //
        //   const mapDispatchToProps = { foo }
        //
        result = this.getMapDispatchToProps().getMember(name).asSink()
        or
        //
        //   const mapDispatchToProps = dispatch => ( { foo } )
        //
        result = this.getMapDispatchToProps().getReturn().getMember(name).asSink()
        or
        // Explicitly bound by bindActionCreators:
        //
        //   const mapDispatchToProps = dispatch => bindActionCreators({ foo }, dispatch);
        //
        exists(BindActionCreatorsCall bind |
          bind.flowsTo(this.getMapDispatchToProps().getReturn().asSink()) and
          result = bind.getOptionArgument(0, name)
        )
      }

      /**
       * Gets the React component decorated by this call, if one can be determined.
       */
      ReactComponent getReactComponent() {
        exists(DataFlow::SourceNode component | component = result.getAComponentCreatorReference() |
          component.flowsTo(this.getAComponentTransformer().getACall().getArgument(0))
          or
          component.(DataFlow::ClassNode).getADecorator() = this.getAComponentTransformer()
        )
      }
    }

    /** A call to `connect`. */
    private class RealConnectFunction extends ConnectCall {
      RealConnectFunction() {
        this = API::moduleImport("react-redux").getMember("connect").getACall()
      }

      override API::Node getMapStateToProps() { result = this.getParameter(0) }

      override API::Node getMapDispatchToProps() { result = this.getParameter(1) }
    }

    /**
     * An API entry point corresponding to a `connect` function which we couldn't recognize exactly.
     *
     * The `connect` call is recognized based on an argument being named either `mapStateToProps` or `mapDispatchToProps`.
     * Used to catch cases where the `connect` function was not recognized by API graphs (usually because of it being
     * wrapped in another function, which API graphs won't look through).
     */
    private class HeuristicConnectEntryPoint extends API::EntryPoint {
      HeuristicConnectEntryPoint() { this = "react-redux-connect" }

      override DataFlow::SourceNode getASource() {
        exists(DataFlow::CallNode call |
          call.getAnArgument().asExpr().(Identifier).getName() =
            ["mapStateToProps", "mapDispatchToProps"] and
          // exclude genuine calls to avoid duplication
          not call = DataFlow::moduleMember("react-redux", "connect").getACall() and
          result = call.getCalleeNode().getALocalSource()
        )
      }
    }

    /** A heuristic call to `connect`, recognized by it taking arguments named `mapStateToProps` and `mapDispatchToProps`. */
    private class HeuristicConnectFunction extends ConnectCall {
      HeuristicConnectFunction() { this = any(HeuristicConnectEntryPoint e).getANode().getACall() }

      override API::Node getMapStateToProps() {
        result = this.getAParameter() and
        result.asSink().asExpr().(Identifier).getName() = "mapStateToProps"
      }

      override API::Node getMapDispatchToProps() {
        result = this.getAParameter() and
        result.asSink().asExpr().(Identifier).getName() = "mapDispatchToProps"
      }
    }

    /**
     * A step from the return value of `mapStateToProps` to a `props` access.
     */
    private class StateToPropsStep extends StateStep {
      override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
        exists(ConnectCall call |
          pred = call.getMapStateToProps().getReturn().asSink() and
          succ = call.getReactComponent().getADirectPropsAccess()
        )
      }
    }

    /**
     * Holds if `pred -> succ` is a step from `mapDispatchToProps` to a `props` property access.
     */
    predicate dispatchToPropsStep(DataFlow::Node pred, DataFlow::Node succ) {
      exists(ConnectCall call, string member |
        pred = call.getDispatchPropNode(member) and
        succ = call.getReactComponent().getAPropRead(member)
      )
    }

    /** The first argument to `mapDispatchToProps` as a source of the `dispatch` function */
    private class MapDispatchToPropsArg extends DispatchFunctionSource {
      MapDispatchToPropsArg() { this = any(ConnectCall c).getMapDispatchToProps().getParameter(0) }
    }

    /** If `mapDispatchToProps` is an object, each method's return value is dispatched. */
    private class MapDispatchToPropsMember extends DispatchedValueSink {
      MapDispatchToPropsMember() {
        this = any(ConnectCall c).getMapDispatchToProps().getAMember().getReturn()
      }
    }

    /** The first argument to `mapStateToProps` as an access to the root state. */
    private class MapStateToPropsStateSource extends RootStateSource {
      MapStateToPropsStateSource() {
        this = any(ConnectCall c).getMapStateToProps().getParameter(0)
      }
    }
  }

  private module Reselect {
    /**
     * A call to `createSelector`.
     *
     * Such calls have two forms. The single-argument version is simply a memoized function wrapper:
     *
     * ```js
     *   createSelector(state => state.foo)
     * ```
     *
     * If multiple arguments are used, each callback independently maps over the state, and last
     * callback collects all the intermediate results into the final result:
     *
     * ```js
     *   createSelector(
     *     state => state.foo,
     *     state => state.bar,
     *     ([foo, bar]) => {...}
     *   )
     * ```
     *
     * Although selectors can work on any data, not just the Redux state, they are in practice only used
     * with the state.
     */
    class CreateSelectorCall extends API::CallNode {
      CreateSelectorCall() {
        this =
          API::moduleImport(["reselect", "@reduxjs/toolkit"]).getMember("createSelector").getACall()
      }

      /** Gets the `i`th selector callback, that is, a callback other than the result function. */
      API::Node getSelectorFunction(int i) {
        // When there are multiple callbacks, exclude the last one
        result = this.getParameter(i) and
        (i = 0 or i < this.getNumArgument() - 1)
        or
        // Selector functions may be given as an array
        exists(DataFlow::ArrayCreationNode array |
          array.flowsTo(this.getArgument(0)) and
          result.getAValueReachableFromSource() = array.getElement(i)
        )
      }
    }

    /** The state argument to a selector */
    private class SelectorStateArg extends RootStateSource {
      SelectorStateArg() { this = any(CreateSelectorCall c).getSelectorFunction(_).getParameter(0) }
    }

    /** A flow step between the callbacks of `createSelector` or out of its final selector. */
    private class CreateSelectorStep extends StateStep {
      override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
        // Return value of `i`th callback flows to the `i`th parameter of the last callback.
        exists(CreateSelectorCall call, int index |
          call.getNumArgument() > 1 and
          pred = call.getSelectorFunction(index).getReturn().asSink() and
          succ = call.getLastParameter().getParameter(index).asSource()
        )
        or
        // The result of the last callback is the final result
        exists(CreateSelectorCall call |
          pred = call.getLastParameter().getReturn().asSink() and
          succ = call
        )
      }
    }
  }

  /** For testing only. */
  module Internal {
    predicate getRootStateAccessPath = rootStateAccessPath/1;
  }
}
